# Copyright 2015 IBM Corp.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import os

from oslo_log import log as logging
from oslo_utils import encodeutils

from trove.common import cfg
from trove.common.db import models
from trove.common import exception
from trove.common.i18n import _
from trove.common import instance as rd_instance
from trove.common.stream_codecs import PropertiesCodec
from trove.common import utils
from trove.guestagent.common.configuration import ConfigurationManager
from trove.guestagent.common.configuration import ImportOverrideStrategy
from trove.guestagent.common import guestagent_utils
from trove.guestagent.common import operating_system
from trove.guestagent.datastore.experimental.db2 import system
from trove.guestagent.datastore import service

CONF = cfg.CONF
LOG = logging.getLogger(__name__)
MOUNT_POINT = CONF.db2.mount_point
FAKE_CFG = os.path.join(MOUNT_POINT, "db2.cfg.fake")
DB2_DEFAULT_CFG = os.path.join(MOUNT_POINT, "db2_default_dbm.cfg")


class DB2App(object):
    """
    Handles installation and configuration of DB2
    on a Trove instance.
    """
    def __init__(self, status, state_change_wait_time=None):
        LOG.debug("Initialize DB2App.")
        self.state_change_wait_time = (
            state_change_wait_time if state_change_wait_time else
            CONF.state_change_wait_time
        )
        LOG.debug("state_change_wait_time = %s.", self.state_change_wait_time)
        self.status = status
        self.dbm_default_config = {}
        self.init_config()
        '''
        If DB2 guest agent has been configured for online backups,
        every database that is created will be configured for online
        backups. Since online backups are done using archive logging,
        we need to create a directory to store the archived logs.
        '''
        if CONF.db2.backup_strategy == 'DB2OnlineBackup':
            create_db2_dir(system.DB2_ARCHIVE_LOGS_DIR)

    def init_config(self):
        if not operating_system.exists(MOUNT_POINT, True):
            operating_system.create_directory(MOUNT_POINT,
                                              system.DB2_INSTANCE_OWNER,
                                              system.DB2_INSTANCE_OWNER,
                                              as_root=True)
        """
        The database manager configuration file - db2systm is stored  under the
        /home/db2inst1/sqllib directory. To update the configuration
        parameters, DB2 recommends using the command - UPDATE DBM CONFIGURATION
        commands instead of directly updating the config file.

        The existing PropertiesCodec implementation has been reused to handle
        text-file operations. Configuration overrides are implemented using
        the ImportOverrideStrategy of the guestagent configuration manager.
        """
        LOG.debug("Initialize DB2 configuration")
        revision_dir = (
            guestagent_utils.build_file_path(
                os.path.join(MOUNT_POINT,
                             os.path.dirname(system.DB2_INSTANCE_OWNER)),
                ConfigurationManager.DEFAULT_STRATEGY_OVERRIDES_SUB_DIR)
        )
        if not operating_system.exists(FAKE_CFG):
            operating_system.write_file(FAKE_CFG, '', as_root=True)
            operating_system.chown(FAKE_CFG, system.DB2_INSTANCE_OWNER,
                                   system.DB2_INSTANCE_OWNER, as_root=True)
        self.configuration_manager = (
            ConfigurationManager(FAKE_CFG, system.DB2_INSTANCE_OWNER,
                                 system.DB2_INSTANCE_OWNER,
                                 PropertiesCodec(delimiter='='),
                                 requires_root=True,
                                 override_strategy=ImportOverrideStrategy(
                                     revision_dir, "cnf"))
        )
        '''
        Below we are getting the database manager default configuration and
        saving it to the DB2_DEFAULT_CFG file. This is done to help with
        correctly resetting the configurations to the original values when
        user wants to detach a user-defined configuration group from an
        instance. DB2 provides a command to reset the database manager
        configuration parameters (RESET DBM CONFIGURATION) but this command
        resets all the configuration parameters to the system defaults. When
        we build a DB2 guest image there are certain configurations
        parameters like SVCENAME which we set so that the instance can start
        correctly. Hence resetting this value to the system default will
        render the instance in an unstable state. Instead, the recommended
        way for resetting a subset of configuration parameters is to save
        the output of GET DBM CONFIGURATION of the original configuration
        and then call UPDATE DBM CONFIGURATION to reset the value.
          http://www.ibm.com/support/knowledgecenter/SSEPGG_10.5.0/
        com.ibm.db2.luw.admin.cmd.doc/doc/r0001970.html
        '''
        if not operating_system.exists(DB2_DEFAULT_CFG):
            run_command(system.GET_DBM_CONFIGURATION % {
                "dbm_config": DB2_DEFAULT_CFG})
        self.process_default_dbm_config()

    def process_default_dbm_config(self):
        """
        Once the default database manager configuration is saved to
        DB2_DEFAULT_CFG, we try to store the configuration parameters
        and values into a dictionary object, dbm_default_config. For
        example, a sample content of the database manager configuration
        file looks like this:
         Buffer pool                         (DFT_MON_BUFPOOL) = OFF
        We need to process this so that we key it on the configuration
        parameter DFT_MON_BUFPOOL.
        """
        with open(DB2_DEFAULT_CFG) as cfg_file:
            for line in cfg_file:
                if '=' in line:
                    item = line.rstrip('\n').split(' = ')
                    fIndex = item[0].rfind('(')
                    lIndex = item[0].rfind(')')
                    if fIndex > -1:
                        param = item[0][fIndex + 1: lIndex]
                        value = item[1]
                        '''
                        Some of the configuration parameters have the keyword
                        AUTOMATIC to indicate that DB2 will automatically
                        adjust the setting depending on system resources.
                        For some configuration parameters, DB2 also allows
                        setting a starting value along with the AUTOMATIC
                        setting. In the configuration parameter listing,
                        this is displayed as:
                        MON_HEAP_SZ = AUTOMATIC(90)
                        This can be set using the following command:
                        db2 update dbm cfg using mon_heap_sz 90 automatic
                        '''
                        if not value:
                            value = 'NULL'
                        elif 'AUTOMATIC' in value:
                            fIndex = item[1].rfind('(')
                            lIndex = item[1].rfind(')')
                            if fIndex > -1:
                                default_value = item[1][fIndex + 1: lIndex]
                                value = default_value + " AUTOMATIC"
                        self.dbm_default_config.update({param: value})

    def update_hostname(self):
        """
        When DB2 server is installed, it uses the hostname of the
        instance were the image was built. This needs to be updated
        to reflect the guest instance.
        """
        LOG.debug("Update the hostname of the DB2 instance.")
        try:
            run_command(system.UPDATE_HOSTNAME,
                        superuser='root')
        except exception.ProcessExecutionError:
            raise RuntimeError(_("Command to update the hostname failed."))

    def change_ownership(self, mount_point):
        """
        When DB2 server instance is installed, it does not have the
        DB2 local database directory created (/home/db2inst1/db2inst1).
        This gets created when we mount the cinder volume. So we need
        to change ownership of this directory to the DB2 instance user
        - db2inst1.
        """
        LOG.debug("Changing ownership of the DB2 data directory.")
        try:
            operating_system.chown(mount_point,
                                   system.DB2_INSTANCE_OWNER,
                                   system.DB2_INSTANCE_OWNER,
                                   recursive=False, as_root=True)
        except exception.ProcessExecutionError:
            raise RuntimeError(_(
                "Command to change ownership of  DB2 data directory failed."))

    def _enable_db_on_boot(self):
        LOG.debug("Enable DB on boot.")
        try:
            run_command(system.ENABLE_AUTOSTART)
        except exception.ProcessExecutionError:
            raise RuntimeError(_(
                "Command to enable DB2 server on boot failed."))

    def _disable_db_on_boot(self):
        LOG.debug("Disable DB2 on boot.")
        try:
            run_command(system.DISABLE_AUTOSTART)
        except exception.ProcessExecutionError:
            raise RuntimeError(_(
                "Command to disable DB2 server on boot failed."))

    def start_db_with_conf_changes(self, config_contents):
        LOG.info("Starting DB2 with configuration changes.")
        self.configuration_manager.save_configuration(config_contents)
        self.start_db(True)

    def start_db(self, update_db=False):
        LOG.debug("Start the DB2 server instance.")
        self._enable_db_on_boot()
        try:
            run_command(system.START_DB2)
        except exception.ProcessExecutionError:
            pass

        if not self.status.wait_for_real_status_to_change_to(
                rd_instance.ServiceStatuses.RUNNING,
                self.state_change_wait_time, update_db):
            LOG.error("Start of DB2 server instance failed.")
            self.status.end_restart()
            raise RuntimeError(_("Could not start DB2."))

    def stop_db(self, update_db=False, do_not_start_on_reboot=False):
        LOG.debug("Stop the DB2 server instance.")
        if do_not_start_on_reboot:
            self._disable_db_on_boot()
        try:
            run_command(system.STOP_DB2)
        except exception.ProcessExecutionError:
            pass

        if not (self.status.wait_for_real_status_to_change_to(
                rd_instance.ServiceStatuses.SHUTDOWN,
                self.state_change_wait_time, update_db)):
            LOG.error("Could not stop DB2.")
            self.status.end_restart()
            raise RuntimeError(_("Could not stop DB2."))

    def restart(self):
        LOG.debug("Restarting DB2 server instance.")
        try:
            self.status.begin_restart()
            self.stop_db()
            self.start_db()
        finally:
            self.status.end_restart()

    def update_overrides(self, context, overrides, remove=False):
        if overrides:
            self.apply_overrides(overrides)

    def remove_overrides(self):
        config = self.configuration_manager.get_user_override()
        self._reset_config(config)
        self.configuration_manager.remove_user_override()

    def apply_overrides(self, overrides):
        self._apply_config(overrides)
        self.configuration_manager.apply_user_override(overrides)

    def _update_dbm_config(self, param, value):
        try:
            run_command(
                system.UPDATE_DBM_CONFIGURATION % {
                    "parameter": param,
                    "value": value})
        except exception.ProcessExecutionError:
            LOG.exception("Failed to update config %s", param)
            raise

    def _reset_config(self, config):
        try:
            for k, v in config.items():
                default_cfg_value = self.dbm_default_config[k]
                self._update_dbm_config(k, default_cfg_value)
        except Exception:
            LOG.exception("DB2 configuration reset failed.")
            raise RuntimeError(_("DB2 configuration reset failed."))
        LOG.info("DB2 configuration reset completed.")

    def _apply_config(self, config):
        try:
            for k, v in config.items():
                self._update_dbm_config(k, v)
        except Exception:
            LOG.exception("DB2 configuration apply failed")
            raise RuntimeError(_("DB2 configuration apply failed"))
        LOG.info("DB2 config apply completed.")


class DB2AppStatus(service.BaseDbStatus):
    """
    Handles all of the status updating for the DB2 guest agent.
    """
    def _get_actual_db_status(self):
        LOG.debug("Getting the status of the DB2 server instance.")
        try:
            out, err = utils.execute_with_timeout(
                system.DB2_STATUS, shell=True)
            if "0" not in out:
                return rd_instance.ServiceStatuses.RUNNING
            else:
                return rd_instance.ServiceStatuses.SHUTDOWN
        except exception.ProcessExecutionError:
            LOG.exception("Error getting the DB2 server status.")
            return rd_instance.ServiceStatuses.CRASHED


def run_command(command, superuser=system.DB2_INSTANCE_OWNER,
                timeout=system.TIMEOUT):
    return utils.execute_with_timeout("sudo", "su", "-", superuser, "-c",
                                      command, timeout=timeout)


def create_db2_dir(dir_name):
    if not operating_system.exists(dir_name, True):
        operating_system.create_directory(dir_name,
                                          system.DB2_INSTANCE_OWNER,
                                          system.DB2_INSTANCE_OWNER,
                                          as_root=True)


def remove_db2_dir(dir_name):
    operating_system.remove(dir_name,
                            force=True,
                            as_root=True)


class DB2Admin(object):
    """
    Handles administrative tasks on the DB2 instance.
    """
    def create_database(self, databases):
        """Create the given database(s)."""
        dbName = None
        db_create_failed = []
        LOG.debug("Creating DB2 databases.")
        for item in databases:
            mydb = models.DatastoreSchema.deserialize(item)
            mydb.check_create()
            dbName = mydb.name
            LOG.debug("Creating DB2 database: %s.", dbName)
            try:
                run_command(system.CREATE_DB_COMMAND % {'dbname': dbName})
            except exception.ProcessExecutionError:
                LOG.exception(
                    "There was an error creating database: %s.", dbName)
                db_create_failed.append(dbName)

            '''
            Configure each database to do archive logging for online
            backups. Once the database is configured, it will go in to a
            BACKUP PENDING state. In this state, the database will not
            be accessible for any operations. To get the database back to
            normal mode, we have to do a full offline backup as soon as we
            configure it for archive logging.
            '''
            try:
                if CONF.db2.backup_strategy == 'DB2OnlineBackup':
                    run_command(system.UPDATE_DB_LOG_CONFIGURATION % {
                        'dbname': dbName})
                    run_command(system.RECOVER_FROM_BACKUP_PENDING_MODE % {
                        'dbname': dbName})
            except exception.ProcessExecutionError:
                LOG.exception(
                    "There was an error while configuring the database for "
                    "online backup: %s.", dbName)

        if len(db_create_failed) > 0:
            LOG.exception("Creating the following databases failed: %s.",
                          db_create_failed)

    def delete_database(self, database):
        """Delete the specified database."""
        dbName = None
        try:
            mydb = models.DatastoreSchema.deserialize(database)
            mydb.check_delete()
            dbName = mydb.name
            LOG.debug("Deleting DB2 database: %s.", dbName)
            run_command(system.DELETE_DB_COMMAND % {'dbname': dbName})
        except exception.ProcessExecutionError:
            LOG.exception(
                "There was an error while deleting database:%s.", dbName)
            raise exception.GuestError(original_message=_(
                "Unable to delete database: %s.") % dbName)

    def list_databases(self, limit=None, marker=None, include_marker=False):
        LOG.debug("Listing all the DB2 databases.")
        databases = []
        next_marker = None

        try:
            out, err = run_command(system.LIST_DB_COMMAND)
            dblist = out.split()
            result = iter(dblist)
            count = 0

            if marker is not None:
                try:
                    item = next(result)
                    while item != marker:
                        item = next(result)

                    if item == marker:
                        marker = None
                except StopIteration:
                    pass

            try:
                item = next(result)
                while item:
                    count = count + 1
                    if (limit and count <= limit) or limit is None:
                        db2_db = models.DatastoreSchema(name=item)
                        LOG.debug("database = %s .", item)
                        next_marker = db2_db.name
                        databases.append(db2_db.serialize())
                        item = next(result)
                    else:
                        next_marker = None
                        break
            except StopIteration:
                next_marker = None
            LOG.debug("databases = %s.", str(databases))
        except exception.ProcessExecutionError as pe:
            err_msg = encodeutils.exception_to_unicode(pe)
            LOG.exception("An error occurred listing databases: %s.",
                          err_msg)
        return databases, next_marker

    def create_user(self, users):
        LOG.debug("Creating user(s) for accessing DB2 database(s).")
        try:
            for item in users:
                user = models.DatastoreUser.deserialize(item)
                user.check_create()
                try:
                    LOG.debug("Creating OS user: %s.", user.name)
                    utils.execute_with_timeout(
                        system.CREATE_USER_COMMAND % {
                            'login': user.name, 'login': user.name,
                            'passwd': user.password}, shell=True)
                except exception.ProcessExecutionError as pe:
                    LOG.exception("Error creating user: %s.", user.name)
                    continue

                for database in user.databases:
                    mydb = models.DatastoreSchema.deserialize(database)
                    try:
                        LOG.debug("Granting user: %(user)s access to "
                                  "database: %(db)s.",
                                  {'user': user.name, 'db': mydb.name})
                        run_command(system.GRANT_USER_ACCESS % {
                            'dbname': mydb.name, 'login': user.name})
                    except exception.ProcessExecutionError as pe:
                        LOG.debug("Error granting user: %(user)s access to "
                                  "database: %(db)s.",
                                  {'user': user.name, 'db': mydb.name})
                        LOG.debug(pe)
        except exception.ProcessExecutionError as pe:
            LOG.exception("An error occurred creating users: %s.", str(pe))

    def delete_user(self, user):
        LOG.debug("Delete a given user.")
        db2_user = models.DatastoreUser.deserialize(user)
        db2_user.check_delete()
        userName = db2_user.name
        user_dbs = db2_user.databases
        LOG.debug("For user %(user)s, databases to be deleted = %(dbs)r.",
                  {'user': userName, 'dbs': user_dbs})

        if len(user_dbs) == 0:
            databases = self.list_access(db2_user.name, None)
        else:
            databases = user_dbs

        LOG.debug("databases for user = %r.", databases)
        for database in databases:
            mydb = models.DatastoreSchema.deserialize(database)
            try:
                run_command(system.REVOKE_USER_ACCESS % {
                    'dbname': mydb.name,
                    'login': userName})
                LOG.debug("Revoked access for user:%(user)s on "
                          "database:%(db)s.",
                          {'user': userName, 'db': mydb.name})
            except exception.ProcessExecutionError as pe:
                LOG.debug("Error occurred while revoking access to %s.",
                          mydb.name)
            try:
                utils.execute_with_timeout(system.DELETE_USER_COMMAND % {
                    'login': db2_user.name.lower()}, shell=True)
            except exception.ProcessExecutionError as pe:
                LOG.exception(
                    "There was an error while deleting user: %s.", pe)
                raise exception.GuestError(original_message=_(
                    "Unable to delete user: %s.") % userName)

    def list_users(self, limit=None, marker=None, include_marker=False):
        LOG.debug(
            "List all users for all the databases in a DB2 server instance.")
        users = []
        user_map = {}
        next_marker = None
        count = 0

        databases, marker = self.list_databases()
        for database in databases:
            db2_db = models.DatastoreSchema.deserialize(database)
            out = None
            try:
                out, err = run_command(
                    system.LIST_DB_USERS % {'dbname': db2_db.name})
            except exception.ProcessExecutionError:
                LOG.debug(
                    "There was an error while listing users for database: %s.",
                    db2_db.name)
                continue

            userlist = []
            for item in out.split('\n'):
                LOG.debug("item = %r", item)
                user = item.split() if item != "" else None
                LOG.debug("user = %r", user)
                if (user is not None
                    and (user[0] not in cfg.get_ignored_users()
                         and user[1] == 'Y')):
                    userlist.append(user[0])
            result = iter(userlist)

            if marker is not None:
                try:
                    item = next(result)
                    while item != marker:
                        item = next(result)

                    if item == marker:
                        marker = None
                except StopIteration:
                    pass

            try:
                item = next(result)

                while item:
                    '''
                    Check if the user has already been discovered. If so,
                    add this database to the database list for this user.
                    '''
                    if item in user_map:
                        db2user = user_map.get(item)
                        db2user.databases = db2_db.name
                        item = next(result)
                        continue
                    '''
                     If this user was not previously discovered, then add
                     this to the user's list.
                    '''
                    count = count + 1
                    if (limit and count <= limit) or limit is None:
                        db2_user = models.DatastoreUser(name=item,
                                                        databases=db2_db.name)
                        users.append(db2_user.serialize())
                        user_map.update({item: db2_user})
                        item = next(result)
                    else:
                        next_marker = None
                        break
            except StopIteration:
                next_marker = None

            if count == limit:
                break
        return users, next_marker

    def get_user(self, username, hostname):
        LOG.debug("Get details of a given database user.")
        user = self._get_user(username, hostname)
        if not user:
            return None
        return user.serialize()

    def _get_user(self, username, hostname):
        LOG.debug("Get details of a given database user %s.", username)
        user = models.DatastoreUser(name=username)
        databases, marker = self.list_databases()
        out = None
        for database in databases:
            db2_db = models.DatastoreSchema.deserialize(database)
            try:
                out, err = run_command(
                    system.LIST_DB_USERS % {'dbname': db2_db.name})
            except exception.ProcessExecutionError:
                LOG.debug(
                    "Error while trying to get the users for database: %s.",
                    db2_db.name)
                continue

            for item in out.split('\n'):
                user_access = item.split() if item != "" else None
                if (user_access is not None and
                        user_access[0].lower() == username.lower() and
                        user_access[1] == 'Y'):
                    user.databases = db2_db.name
                    break
        return user

    def list_access(self, username, hostname):
        """
           Show all the databases to which the user has more than
           USAGE granted.
        """
        LOG.debug("Listing databases that user: %s has access to.", username)
        user = self._get_user(username, hostname)
        return user.databases
